import { stringEqual } from "../../common/util";
import { Mutable } from "../../common/types";
import * as t from "./types";
import { getUuid } from "./util";
import { domImpl } from "./xml";

class MDMObject implements t.IMDMObject {

    public readonly objectFlag: t.ObjectFlags;
    public readonly parent: t.IMDMObject;
    public readonly reference?: t.IMDMObject | undefined;
    public readonly uuid: string;
    public readonly properties: t.Properties;
    public readonly name: string;
    public readonly document: t.BaseMDMDocument;

    public constructor(document: t.BaseMDMDocument, parent: t.IMDMObject, flag: t.ObjectFlags, name: string, uuid?: string) {
        this.objectFlag = flag;
        this.parent = parent;
        this.document = document;
        this.uuid = uuid ?? getUuid();
        this.name = name;
        this.properties = new Properties(document);
    }

}

class MDMListOf<T> extends Array<T> implements t.MDMListOf<T> {

    public readonly deleted: T[];

    public constructor(items?: readonly T[], deleted?: readonly T[]) {
        super();
        if (items) {
            this.push(...items);
        }
        this.deleted = deleted ? deleted.slice() : [];
    }

    public update(item: T): void;
    public update(items: T[]): void;
    public update(arg: T | T[]): void {
        if (Array.isArray(arg)) {
            this.push(...arg);
        }
        else {
            this.push(arg);
        }
    }

    public updateDeleted(item: T): void;
    public updateDeleted(items: T[]): void;
    public updateDeleted(arg: T | T[]): void {
        if (Array.isArray(arg)) {
            (this.deleted as T[]).push(...arg);
        }
        else {
            (this.deleted as T[]).push(arg);
        }
    }

}

class ContextMap<T> extends MDMListOf<t.ContextMapItem<T>> implements t.ContextMap<T> {

    public constructor(items?: readonly t.ContextMapItem<T>[], deleted?: readonly t.ContextMapItem<T>[]) {
        super(items, deleted);
    }

    public getItem(context: string): T | undefined;
    public getItem(predicate: (element: T) => boolean): T | undefined;
    public getItem(arg: string | ((element: T) => boolean)): T | undefined {
        const exist = this.find(e => typeof arg === "string" ? stringEqual(e.context, arg) : arg(e.value));
        if (exist) {
            return exist.value;
        }
    }

    public setItem(context: string, element: T): void {
        const exist = this.find(e => stringEqual(e.context, context));
        if (exist) {
            (exist as Mutable<t.ContextMapItem<T>>).value = element;
        }
        else {
            this.push({ context, value: element });
        }
    }

    public getAllItems(predicate: (element: T) => boolean): t.ContextMapItem<T>[] {
        const result: t.ContextMapItem<T>[] = [];
        for (const item of this) {
            if (predicate(item.value)) {
                result.push(item);
            }
        }
        return result;
    }

    public removeItem(context: string): void;
    public removeItem(predicate: (element: T) => boolean): void;
    public removeItem(arg: string | ((element: T) => boolean)): void {
        const findIndex = this.findIndex(e => typeof arg === "string" ? stringEqual(e.context, arg) : arg(e.value));
        if (findIndex > -1) {
            this.splice(findIndex, 1);
        }
    }

    public countItem(context: string, callback: (item: T) => number): number {
        const item = this.getItem(context);
        return item ? callback(item) : 0;
    }

}

function createProperty(name: string, context: string, value?: t.PropertyValue | null, dataSource?: string): t.Property {
    const obj = { name, context, value, dataSource, versioned: true } as Mutable<t.Property>;
    if (value !== undefined) {
        if (typeof value === "number") {
            obj.valueType = Number.isInteger(value) ? 3 : 5;
        }
        else if (typeof value === "string") {
            obj.valueType = 8;
        }
        else if (typeof value === "boolean") {
            obj.valueType = 11;
        }
        else {
            obj.valueType = 9;
        }
    }
    return obj;
}

class Properties extends ContextMap<t.Property[]> implements t.Properties {

    public readonly objectFlag: t.ObjectFlags.properties;
    public readonly id?: string | undefined;
    public readonly name?: string | undefined;
    public readonly document: t.BaseMDMDocument;

    public constructor(document: t.BaseMDMDocument, id?: string, name?: string, items?: readonly t.ContextMapItem<t.Property[]>[]) {
        super(items);
        this.document = document;
        this.objectFlag = t.ObjectFlags.properties;
        this.id = id;
        this.name = name;
    }

    public addNew(contextAddTo: string, property: t.Property): t.Property;
    public addNew(contextAddTo: string, name: string, context: string, value?: t.PropertyValue, dataSource?: string): t.Property;
    public addNew(contextAddTo: string, arg: string | t.Property, context: string = "Question", value?: t.PropertyValue, dataSource?: string): t.Property {
        let prop: t.Property;
        if (typeof arg === "string") {
            prop = createProperty(arg, context, value, dataSource);
        }
        else {
            prop = arg;
        }
        const exist = this.getItem(contextAddTo);
        if (exist) {
            const existProperty = exist.find(e => stringEqual(e.name, prop.name)) as Mutable<t.Property>;
            if (existProperty) {
                existProperty.value = prop.value;
                prop = existProperty;
            }
            else {
                exist.push(prop);
            }
        }
        else {
            this.update({ context: contextAddTo, value: [prop] });
        }
        return prop;
    }

}

class Templates extends ContextMap<t.Template[]> implements t.Templates {

    public readonly objectFlag: t.ObjectFlags.templates;
    public readonly document: t.BaseMDMDocument;

    public constructor(document: t.BaseMDMDocument, items?: readonly t.ContextMapItem<t.Template[]>[]) {
        super(items);
        this.objectFlag = t.ObjectFlags.templates;
        this.document = document;
    }

    public addNew(document: t.MDMDocument, contextAddTo: string, name: string, context: string, value: string): t.Template {
        let template: t.Template = {
            document,
            objectFlag: t.ObjectFlags.template,
            name,
            context,
            value
        };
        const exist = this.getItem(contextAddTo);
        if (exist) {
            const existTemplate = exist.find(e => stringEqual(e.name, name)) as Mutable<t.Template>;
            if (existTemplate) {
                existTemplate.value = value;
                template = existTemplate;
            }
            else {
                exist.push(template);
            }
        }
        else {
            this.update({ context: contextAddTo, value: [template] });
        }
        return template;
    }

}

class Label extends MDMListOf<t.LabelObject> implements t.Label {

    public readonly context: string;
    public readonly document: t.BaseMDMDocument;

    public constructor(document: t.BaseMDMDocument, context: string, items?: readonly t.LabelObject[]) {
        super(items);
        this.document = document;
        this.context = context;
    }

    public getText(context: string, language: string): string | undefined {
        return this.find(ele => stringEqual(ele.context, context) && stringEqual(ele.language, language))?.text;
    }

    public setText(context: string, language: string, text: string): void {
        const exist = this.find(ele => stringEqual(ele.context, context) && stringEqual(ele.language, language));
        if (exist) {
            (exist as Mutable<t.LabelObject>).text = text;
        }
        else {
            this.push({ context, language, text });
        }
    }

    public removeText(context: string, language: string): void {
        const findIndex = this.findIndex(e => stringEqual(e.context, context) && stringEqual(e.language, language));
        if (findIndex > -1) {
            this.splice(findIndex, 1);
        }
    }

}

class Labels extends ContextMap<t.Label> implements t.Labels {

    public readonly document: t.BaseMDMDocument;

    public constructor(document: t.BaseMDMDocument, entries?: readonly t.ContextMapItem<t.Label>[]) {
        super(entries);
        this.document = document;
    }

    public getText(targetContext: string | undefined, context: string, language: string): string | undefined {
        const item = this.getItem(targetContext ?? this.document.labelTypes.current);
        if (item) {
            return item.getText(context, language);
        }
    }

    public setText(targetContext: string | undefined, context: string, language: string, text: string): void {
        const labelType = targetContext ?? this.document.labelTypes.current;
        const exist = this.getItem(labelType);
        if (exist) {
            exist.setText(context, language, text);
        }
        else {
            const newItem = new Label(this.document, labelType);
            newItem.setText(context, language, text);
            this.setItem(labelType, newItem);
        }
    }

    public clearText(labelType: string): void {
        const index = this.findIndex(e => stringEqual(e.context, labelType));
        if (index > -1) {
            this.splice(index);
        }
    }

}

function createNotes() {
    const obj = new MDMListOf<t.Note>() as t.MDMListOf<t.Note> as Mutable<t.Notes>;
    obj.objectFlag = t.ObjectFlags.notes;
    return obj;
}

class MDMLabeledObject extends MDMObject implements t.IMDMLabeledObject {

    public readonly labels: t.Labels;
    public readonly styles: t.Styles;
    public readonly labelStyles: t.Styles;
    public readonly templates: t.Templates;
    public readonly notes: t.Notes;
    public readonly reference?: t.IMDMLabeledObject | undefined;

    public constructor(document: t.BaseMDMDocument, parent: t.IMDMObject, flag: t.ObjectFlags, name: string, uuid?: string, reference?: t.IMDMLabeledObject) {
        super(document, parent, flag, name, uuid);
        this.labels = new Labels(document);
        this.reference = reference;
        this.styles = new ContextMap();
        this.labelStyles = new ContextMap();
        this.templates = new Templates(document);
        this.notes = createNotes();
    }

}

class Types<T extends t.IMDMObject> extends Map<string, T> implements t.Types<T> {

    public readonly objectFlag: t.ObjectFlags;
    public readonly parent: t.IMDMObject;
    public readonly uuid: string;
    public readonly expanded?: t.Types<T> | undefined;
    public readonly deleted: Map<string, T>;
    public readonly unversioned?: t.Types<T> | undefined;

    public constructor(parent: t.IMDMObject, uuid?: string, entries?: readonly [string, T][]) {
        super();
        this.objectFlag = t.ObjectFlags.types;
        this.parent = parent;
        this.uuid = uuid ?? getUuid();
        this.deleted = new Map();
        if (entries) {
            for (const item of entries) {
                this.set(item[0], item[1]);
            }
        }
    }

    public update(item: T): void {
        this.set(item.uuid, item);
    }

    public updateDeleted(item: T): void {
        this.deleted.set(item.uuid, item);
    }

}

export class BaseMDMDocument implements t.BaseMDMDocument {

    public readonly dom: XMLDocument;
    public readonly properties: t.Properties;
    public readonly templates: t.Templates;
    public readonly definitions: ReadonlyMap<string, t.IMDMLabeledObject>;
    public readonly fields: t.Types<t.IMDMLabeledObject>;
    public readonly types: t.Types<t.MDMElements>;
    public readonly systemVariables: t.Types<t.Class>;
    public readonly saveLogs: t.SaveLogs;
    public readonly lcl: t.LCLMap;
    public readonly url: string;
    public readonly openMode: t.OpenFlags;
    public readonly projectDirectory: string;
    public readonly atoms: readonly string[];
    public readonly mapping: readonly t.VariableInstance[];
    public readonly reference?: t.IMDMLabeledObject | undefined;
    public readonly labels: t.Labels;
    public readonly styles: t.Styles;
    public readonly labelStyles: t.Styles;
    public readonly notes: t.Notes;
    public readonly objectFlag: t.ObjectFlags;
    public readonly parent: t.IMDMObject;
    public readonly uuid: string;
    public readonly name: string;
    public readonly document: t.BaseMDMDocument;

    // properties initialize by factory
    public readonly versions!: t.Versions;
    public readonly currentVersion!: string;
    public readonly mdmVersion!: string;
    public readonly pages!: t.Pages;
    public readonly createdByVersion!: string;
    public readonly lastUpdatedByVersion!: string;
    public readonly systemRouting!: t.Routings;
    public readonly routings!: t.Routings;
    public readonly categoryMap!: t.CategoryMap;
    public readonly aliasMap!: t.AliasMap;
    public readonly dataSources!: t.DataSources;
    public readonly languages!: t.Languages;
    public readonly contexts!: t.Contexts;
    public readonly labelTypes!: t.Contexts;
    public readonly routingContexts!: t.Contexts;
    public readonly scriptTypes!: t.Contexts;

    private _atomCheckSet: Set<string>;

    public constructor() {
        this.objectFlag = t.ObjectFlags.document;
        this.dom = domImpl.createDocument(null, null);
        this.name = "";
        this.uuid = getUuid();
        this.document = this;
        this.parent = this;
        this.properties = new Properties(this);
        this.templates = new Templates(this);
        this.labels = new Labels(this);
        this.styles = new ContextMap();
        this.labelStyles = new ContextMap();
        this.lcl = new Map();
        this.definitions = new Map();
        this.atoms = [];
        this.mapping = [];
        this.notes = createNotes();
        this.saveLogs = [];
        this.fields = new Types(this);
        this.types = new Types(this);
        this.systemVariables = new Types(this);
        this.openMode = t.OpenFlags.readWrite;
        this.url = "";
        this.projectDirectory = "";
        this._atomCheckSet = new Set();
    }

    public setCurrentContext(context: string): void {
        (this.contexts as Mutable<t.Contexts>).current = context;
    }

    public setCurrentLanguage(language: string): void {
        (this.languages as Mutable<t.Languages>).current = language;
    }

    public setCurrentLabelType(labelType: string): void {
        (this.labelTypes as Mutable<t.Contexts>).current = labelType;
    }

    public setCurrentRoutingContext(routingContext: string): void {
        (this.routingContexts as Mutable<t.Contexts>).current = routingContext;
    }

    public getLCLSet(context?: string | undefined): Set<string> | undefined {
        return this.lcl.get(context ? context.toLowerCase() : this.languages.current.toLowerCase());
    }

    public updateLCLSet(language: string, context: string): void {
        const lowerLanguage = language.toLowerCase();
        const lowerContext = context.toLowerCase();
        let set = this.lcl.get(lowerLanguage);
        if (!set) {
            set = new Set<string>();
            set.add(lowerContext);
            (this.lcl as Map<string, Set<string>>).set(lowerLanguage, set);
        }
        else {
            if (!set.has(lowerContext)) {
                set.add(lowerContext);
            }
        }
    }

    public updateCategoryMap(name: string): boolean {
        let lowerName = name.toLowerCase();
        if (this.categoryMap.nameToValueMap.has(lowerName)) {
            return false;
        }
        let value = this.categoryMap.nameToValueMap.size + 1;
        (this.categoryMap.nameToValueMap as Map<string, number>).set(lowerName, value);
        (this.categoryMap.valueToNameMap as Map<number, string>).set(value, lowerName);
        return true;

    }

    public getDefinition<T>(uuid: string | undefined, predicate: (o: any) => o is T): T | undefined {
        let obj = uuid ? this.definitions.get(uuid) : undefined;
        return obj && predicate(obj) ? obj : undefined;
    }

    public getDefinitionByName<T extends t.IMDMLabeledObject>(name: string, predicate: (o: any) => o is T): T[] {
        let result: T[] = [];
        for (const [, variable] of this.definitions) {
            if (stringEqual(variable.name, name) && predicate(variable)) {
                result.push(variable);
            }
        }
        return result;
    }

    public updateDefinition<T extends t.IMDMLabeledObject>(obj: T): void {
        (this.definitions as Map<string, T>).set(obj.uuid, obj);
    }

    public updateAtom(name: string): boolean {
        if (this._atomCheckSet.has(name.toLowerCase())) {
            return false;
        }
        (this.atoms as string[]).push(name);
        this._atomCheckSet.add(name.toLowerCase());
        return true;
    }

}

export const objectAllocator: t.ObjectAllocator = {
    getMDMObjectConstructor: () => MDMObject,
    getMDMListConstructor: () => MDMListOf,
    getContextMapConstructor: () => ContextMap,
    getPropertiesConstructor: () => Properties,
    getTemplatiesConstructor: () => Templates,
    getLabelConstructor: () => Label,
    getLabelsConstructor: () => Labels,
    getTypesConstructor: () => Types,
    getLabeledObjectConstructor: () => MDMLabeledObject,
    getBaseDocumentConstructor: () => BaseMDMDocument,
};

